/*
 *
 *  *    Copyright 2020-2021 Luter.me
 *  *
 *  *    Licensed under the Apache License, Version 2.0 (the "License");
 *  *    you may not use this file except in compliance with the License.
 *  *    You may obtain a copy of the License at
 *  *
 *  *      http://www.apache.org/licenses/LICENSE-2.0
 *  *
 *  *    Unless required by applicable law or agreed to in writing, software
 *  *    distributed under the License is distributed on an "AS IS" BASIS,
 *  *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  *    See the License for the specific language governing permissions and
 *  *    limitations under the License.
 *
 */

package com.luter.heimdall.core.manager;

import com.luter.heimdall.core.config.ConfigManager;
import com.luter.heimdall.core.config.HeimdallProperties;
import com.luter.heimdall.core.config.options.TokenTypeOptions;
import com.luter.heimdall.core.context.WebContextHolder;
import com.luter.heimdall.core.cookie.SimpleCookie;
import com.luter.heimdall.core.details.UserDetails;
import com.luter.heimdall.core.exception.HeimdallException;
import com.luter.heimdall.core.exception.HeimdallUnauthenticatedException;
import com.luter.heimdall.core.jwt.JwtProcessor;
import com.luter.heimdall.core.listener.AbstractAuthEvent;
import com.luter.heimdall.core.token.SimpleToken;
import com.luter.heimdall.core.token.id.IdGenerator;
import com.luter.heimdall.core.token.id.UUIDIdGenerator;
import com.luter.heimdall.core.token.store.TokenStore;
import com.luter.heimdall.core.utils.StrUtils;
import com.luter.heimdall.core.utils.crypto.Md5Util;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.slf4j.Logger;

import java.time.Duration;
import java.util.Collection;

import static org.slf4j.LoggerFactory.getLogger;

/**
 * 默认 认证管理器
 *
 * @author luter
 */
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true, fluent = true)
public class HeimdallAuthenticationManager extends AbstractAuthEvent implements AuthenticationManager {
    /**
     * The constant log.
     */
    private static final transient Logger log = getLogger(HeimdallAuthenticationManager.class);
    /**
     * Token 缓存
     */
    private TokenStore tokenStore;
    /**
     * Web 上下文处理
     */
    private WebContextHolder webContextHolder;
    /**
     * Jwt token 处理器，实现对 jwt 的加密和解析验证。jwt 模式下需要
     */
    private JwtProcessor jwtProcessor;
    /**
     * ID 生成器 ，默认:UUD 生成器
     */
    private IdGenerator idGenerator;

    /**
     * Instantiates a new Auth way authentication manager.
     *
     * @param webContextHolder the web context holder
     */
    public HeimdallAuthenticationManager(WebContextHolder webContextHolder) {
        this.idGenerator = new UUIDIdGenerator();
        if (null == webContextHolder) {
            throw new IllegalArgumentException("WebContextHolder must not be null");
        }
        this.webContextHolder = webContextHolder;
    }

    /**
     * Instantiates a new Auth way authentication manager.
     *
     * @param tokenStore       the token store
     * @param webContextHolder the web context holder
     */
    public HeimdallAuthenticationManager(TokenStore tokenStore, WebContextHolder webContextHolder) {
        this(webContextHolder);
        this.tokenStore = tokenStore;
    }

    @Override
    public String login(UserDetails userDetails) {
        final HeimdallProperties config = ConfigManager.getConfig();
        return login(userDetails, Duration.ofSeconds(config.getToken().getTimeout()));
    }

    @Override
    public String login(UserDetails userDetails, Duration maxAge) {
        if (null == webContextHolder) {
            throw new IllegalArgumentException("webContextHolder must not be null");
        }
        final HeimdallProperties config = ConfigManager.getConfig();
        //生成 ID
        final String tokenId = idGenerator.generate();
        //创建 token,时长与 maxAge 保持一致
        SimpleToken simpleToken = SimpleToken.build(tokenId, maxAge.getSeconds(), userDetails);
        //获取客户端 IP
        final String remoteIp = webContextHolder.getRequest().getRemoteIp();
        //IP MD5 加密存储
        simpleToken.setRia(Md5Util.encrypt(remoteIp));
        //Session 模式
        if (config.getToken().getType().equals(TokenTypeOptions.SESSION)) {
            if (null == tokenStore) {
                throw new HeimdallException("TokenStore must not be null");
            }
            //存入缓存
            tokenStore.saveToken(simpleToken);
            //如果开启 cookie，写 cookie 到客户端
            if (config.getCookie().isEnabled()) {
                SimpleCookie cookie = new SimpleCookie(config.getToken().getName(), tokenId).setMaxAge(maxAge);
                webContextHolder.getResponse().addCookie(cookie);
            }
            //发布登录事件
            onLogin(simpleToken);
            //token Id 返回
            return simpleToken.getId();
        }
        //Jwt 模式
        if (config.getToken().getType().equals(TokenTypeOptions.JWT)) {
            //jwt 模式下，jwt 处理器必须配置
            if (null == jwtProcessor) {
                throw new HeimdallException("Jwt Service must not be null");
            }
            //将 token 加密成 jwt 字符串
            final String generate = jwtProcessor.generate(simpleToken);

            //如果开启了白名单
            if (config.getJwt().isWhiteList()) {
                //但是没配置 tokenStore，异常提醒
                if (null == tokenStore) {
                    throw new HeimdallException(" Jwt White List is enabled,  TokenStore must not be null");
                }
                //缓存 token
                tokenStore.saveToken(simpleToken);
            }
            //发布登录事件
            onLogin(simpleToken);
            //加密 jwt 字符串返回
            return generate;

        }
        throw new HeimdallException("Unsupported Token Type");
    }

    @Override
    public UserDetails getCurrentUser(boolean isThrowEx) {
        final SimpleToken currentToken = getCurrentToken(isThrowEx);
        return null != currentToken ? currentToken.getDetails() : null;
    }

    @Override
    public SimpleToken getCurrentToken(boolean isThrowEx) {
        final HeimdallProperties config = ConfigManager.getConfig();
        if (null == webContextHolder) {
            throw new IllegalArgumentException("webContextHolder must not be null");
        }
        //从请求中解析 token 值
        final String requestToken = webContextHolder.getRequest().resolveToken();
        if (StrUtils.isBlank(requestToken)) {
            log.warn("[getCurrentToken]:: No valid token was resolved from request. isThrowEx = [{}]", isThrowEx);
            if (isThrowEx) {
                throw new HeimdallUnauthenticatedException();
            }
            return null;
        }
        //Session 模式
        if (config.getToken().getType().equals(TokenTypeOptions.SESSION)) {
            //从缓存拿
            final SimpleToken currentToken = tokenStore.getToken(requestToken);
            //没有，或者过期了
            if (null == currentToken || currentToken.hasExpired()) {
                if (isThrowEx) {
                    throw new HeimdallUnauthenticatedException();
                }
                return null;
            }
            log.trace("[getCurrentToken]::isThrowEx = [{}],currentToken = [{}]", isThrowEx, currentToken);
            //续期，store 会判断是否开启了续期功能
            tokenStore.update(currentToken);
            log.debug("[getCurrentToken]:: renew token expire time,isThrowEx = [{}],currentToken = [{}]",
                    isThrowEx, currentToken);
            return currentToken;
        }
        //jwt 模式
        if (config.getToken().getType().equals(TokenTypeOptions.JWT)) {
            //必须配置 jwt 处理器
            if (null == jwtProcessor) {
                throw new HeimdallException("Jwt Service must not be null");
            }
            //校验 jwt token 字符串
            final SimpleToken verifyToken = jwtProcessor.verify(requestToken);
            //如果开启了白名单，还需要与白名单比对
            if (config.getJwt().isWhiteList()) {
                //白名单必须配置 tokenStore
                if (null == tokenStore) {
                    throw new HeimdallException(" Jwt White List is enabled,  TokenStore must not be null");
                }
                //从缓存拿 token
                final SimpleToken token = tokenStore.getToken(verifyToken.getId());
                //没有或者过期了，返回未登录
                if (null == token || token.hasExpired()) {
                    if (isThrowEx) {
                        throw new HeimdallUnauthenticatedException();
                    }
                    return null;
                }
                //返回从 store 里拿到的 token
                return token;
            }
            //没开启白名单，直接返回验证通过后的 token
            return verifyToken;
        }
        throw new HeimdallException("Unsupported Token Type");
    }

    @Override
    public boolean isAuthenticated(boolean isThrowEx) {
        log.trace("[isAuthenticated]::isThrowEx = [{}]", isThrowEx);
        final SimpleToken token = getCurrentToken(isThrowEx);
        log.debug("[isAuthenticated]::token = [{}] ,isThrowEx = [{}]", token, isThrowEx);
        return null != token;
    }

    @Override
    public SimpleToken logout() {
        if (null == webContextHolder) {
            throw new IllegalArgumentException("webContextHolder must not be null");
        }
        //从请求中拿 token 值
        final String requestToken = webContextHolder.getRequest().resolveToken();
        if (StrUtils.isBlank(requestToken)) {
            throw new HeimdallException("Error : Token cannot be empty");
        }
        //是否是需要缓存的 token,不缓存的不需要作废
        if (isTokenStored()) {
            return kickSimpleToken(requestToken);
        }
        throw new HeimdallException("Unsupported Token Type");
    }

    @Override
    public SimpleToken kickToken(String tokenId) {
        log.debug("[HeimdallAuthenticationManager->kickToken]::tokenId = [{}]", tokenId);
        if (isTokenStored()) {
            log.debug("[HeimdallAuthenticationManager->kickToken]::tokenId = [{}]", tokenId);
            return kickSimpleToken(tokenId);
        }
        throw new HeimdallException("Unsupported Token Type");
    }

    @Override
    public int kickPrincipalTokens(UserDetails userDetails) {
        log.debug("[HeimdallAuthenticationManager->kickPrincipalTokens]::userDetails = [{}]", userDetails);
        if (isTokenStored()) {
            if (null != userDetails && StrUtils.isNotBlank(userDetails.getPrincipal())) {
                log.debug("[HeimdallAuthenticationManager->kickPrincipalTokens]::userDetails = [{}]", userDetails);
                final int count = tokenStore.deletePrincipalTokens(userDetails.getPrincipal());
                log.info("[kickPrincipalTokens]::userDetails = [{}],count = [{}]", userDetails, count);
                onPrincipalKick(userDetails, count);
                return count;
            }
        }
        return 0;
    }

    @Override
    public int kickAppActiveTokens(String appId) {
        if (isTokenStored()) {
            return tokenStore.deleteAppTokens(appId);
        }
        return 0;
    }

    @Override
    public Collection<SimpleToken> getPrincipalTokens(UserDetails userDetails) {
        if (isTokenStored()) {
            return tokenStore.getPrincipalTokens(userDetails.getPrincipal());
        }
        return null;
    }

    @Override
    public Collection<SimpleToken> getActiveTokens(String appId) {
        log.info("[getActiveTokens]::appId = [{}]", appId);
        if (isTokenStored()) {
            return tokenStore.getActiveTokens(appId);
        }
        return null;

    }

    @Override
    public Collection<SimpleToken> getActiveTokens() {
        log.info("[getActiveTokens]");
        if (isTokenStored()) {
            return tokenStore.getActiveTokens();
        }
        return null;
    }

    /**
     * Token 是否被缓存
     * <p>
     * Session 模式，或者 Jwt 模式下开启白名单，Token 都会被缓存
     *
     * @return the boolean
     */
    private boolean isTokenStored() {
        final HeimdallProperties config = ConfigManager.getConfig();
        final boolean isSession = config.getToken().getType().equals(TokenTypeOptions.SESSION);
        final boolean isJwt = config.getToken().getType().equals(TokenTypeOptions.JWT);
        final boolean enableWhiteList = config.getJwt().isWhiteList();
        return isSession || (isJwt && enableWhiteList);
    }

    /**
     * 根据 TokenId 作废 token
     *
     * @param tokenId the token id
     * @return the simple token
     */
    private SimpleToken kickSimpleToken(String tokenId) {
        final SimpleToken simpleToken = tokenStore.deleteToken(tokenId);
        if (null != simpleToken) {
            log.info("[kickToken]::tokenId = [{}],simpleToken:[{}]", tokenId, simpleToken);
            onTokenKick(simpleToken);
            log.info("[kickToken]:: token was deleted . tokenId = [{}],simpleToken:[{}]", tokenId, simpleToken);
            return simpleToken;
        }
        log.warn("[kickToken]:: TokenId does not exist in the cache, tokenId = [{}]", tokenId);
        return null;
    }

    ////////    对象代理    //////////
    @Override
    public TokenStore getTokenStore() {
        return this.tokenStore;
    }

    @Override
    public WebContextHolder getWebContextHolder() {
        return this.webContextHolder;
    }

    @Override
    public IdGenerator getIdGenerator() {
        return this.idGenerator;
    }

    @Override
    public JwtProcessor getJwtProcessor() {
        return this.jwtProcessor;
    }
}
